from typing import Callable, Coroutine, Dict, Any, Callable, Dict, ClassVar, Union, Dict
from .qqtypes import (
    Message,
    RobotDict,
    Embed,
    Reference,
    Ark,
    Media,
    MessagePayload,
    MarkdownPayload,
)


import asyncio
import aiohttp
import json
import traceback
import time
import inspect
import logging

from aiohttp import (
    WSMessage,
    ClientWebSocketResponse,
    TCPConnector,
    ClientSession,
    ClientResponse,
)
from ssl import SSLContext
from json.decoder import JSONDecodeError
from Panda.Core.Log import logger as _log


class Token:

    def __init__(self, app_id: str, secret: str):
        """
        :param app_id:
            机器人appid
        :param secret:
            机器人密钥
        """
        self.app_id = app_id
        self.secret = secret
        self.access_token = None
        self.expires_time = 0

    async def check_token(self):
        if self.access_token is None or int(time.time()) >= self.expires_time:
            await self.update_access_token()

    async def update_access_token(self):
        session = ClientSession()
        data = None
        # TODO 增加超时重试
        try:
            async with session.post(
                url="https://bots.qq.com/app/getAppAccessToken",
                timeout=(aiohttp.ClientTimeout(total=20)),
                json={
                    "appId": self.app_id,
                    "clientSecret": self.secret,
                },
            ) as response:
                data = await response.json()
        except asyncio.TimeoutError as e:
            raise Exception("[botpy] access_token TimeoutError:" + str(e))
        finally:
            await session.close()
        if "access_token" not in data or "expires_in" not in data:
            raise Exception(f"[botpy] 获取token失败 {data}")
        _log.info("[botpy] access_token expires_in " + data["expires_in"])
        self.expires_time = int(data["expires_in"]) + int(time.time())
        self.access_token = "QQBot " + data["access_token"]


class Route:
    DOMAIN: ClassVar[str] = "https://api.sgroup.qq.com"

    def __init__(self, method: str, path: str, **parameters: Any) -> None:

        self.method: str = method
        self.path: str = f"{self.DOMAIN}{path}"
        self.params = parameters
        self.url = self.path.format_map(self.params) if self.params else self.path


async def _handle_response(response: ClientResponse) -> Union[Dict[str, Any], str]:
    url = response.request_info.url
    try:
        is_json = response.headers["content-type"] == "application/json"
        # note that when content-type is application/json, aiohttp will directly auto-sub encoding to be utf-8
        data = await response.json() if is_json else await response.text()
    except (KeyError, JSONDecodeError):
        data = None
    X_TPS_TRACE_ID = "X-Tps-trace-Id"
    if response.status in [200, 202, 204]:
        _log.debug(f"[botpy] \n\t[Return] {data}")
        return data
    else:
        _log.error(
            f"[botpy] 接口请求异常，请求连接: {url}, "
            f"错误代码: {response.status}, 返回内容: {data}"
        )
        _message = data["message"] if isinstance(data, dict) else str(data)
        raise TypeError(f"message: {_message}") from None


class Client:
    """``Client` 是一个用于与 QQ频道机器人 Websocket 和 API 交互的类。"""

    def __init__(
        self,
        intents: int,
    ):
        """
        Bot的Websocket实现
            CODE	名称	客户端操作	描述
            0	Dispatch	Receive	服务端进行消息推送
            1	Heartbeat	Send/Receive	客户端或服务端发送心跳
            2	Identify	Send	客户端发送鉴权
            6	Resume	Send	客户端恢复连接
            7	Reconnect	Receive	服务端通知客户端重新连接
            9	Invalid Session	Receive	当identify或resume的时候，如果参数有错，服务端会返回该消息
            10	Hello	Receive	当客户端与网关建立ws连接之后，网关下发的第一条消息
            11	Heartbeat ACK	Receive	当发送心跳成功之后，就会收到该消息
        Args:
            intents (Intents): 通道：机器人需要注册的通道事件code，通过Intents提供的方法获取。
            log_config: 日志配置，可以为dict或.json/.yaml文件路径，会从文件中读取(logging.config.dictConfig)。Default to None（不做更改）
            log_format: 控制台输出格式(logging.basicConfig(format=))。Default to None（不做更改）
            log_level: 控制台输出level。Default to None(不做更改),
            bot_log: bot_log: bot_log: 是否启用bot日志 True/启用 None/禁用拓展 False/禁用拓展+控制台输出
        """
        self.intents: int = intents
        self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()

        self._closed: bool = False
        self.robot: RobotDict = {}
        self.token: Token = None

        self._wss: ClientWebSocketResponse = None
        self.wss_dict: Dict = {}

        self._session: ClientSession = None
        self.session_dict: Dict = {}

        self.heartbeat_interval: int = 30
        self.heartbeat_start = True
        self._can_reconnect = True
        self.heartbeat_task: asyncio.Task = None

        self.parser: Dict[str, Callable[[Any], None]] = {}
        for attr, func in inspect.getmembers(self):
            if attr.startswith("parse_"):
                self.parser[attr[6:].lower()] = func
        _log.debug("\n".join(self.parser.keys()))

    async def ws_connect(self):
        """websocket向服务器端发起链接，并定时发送心跳"""
        # adding SSLContext-containing connector to prevent SSL certificate verify failed error
        async with ClientSession(
            connector=TCPConnector(limit=10, ssl=SSLContext())
        ) as session:
            async with session.ws_connect(self.session_dict["url"]) as wss:
                self._wss = wss  # 第一次赋值

                while True:
                    msg: WSMessage = await wss.receive()
                    _log.warning("[botpy] 接收消息: %s" % msg.data)
                    if msg.type == 1:  # 正常消息
                        await self.on_message(msg.data)
                    elif msg.type == 258:  # Error

                        if self._wss:
                            _log.error(
                                f"[botpy] websocket连接: {self._wss}, 异常信息 : {self._wss.exception}"
                            )
                        await self._wss.close()
                    elif msg.type == 257 or msg.type == 8:  # 257已关闭 8关闭

                        _log.error(
                            "[botpy] 关闭, 返回码: %s" % wss.close_code
                            + ", 返回信息: %s" % msg.extra
                        )
                        if wss.close_code in [4004]:  # 鉴权失败，无法重连
                            _log.critical("[botpy] 鉴权失败,重置token信息重连...")
                        if wss.close_code in [9001, 9005] or not self._can_reconnect:
                            _log.critical("[botpy] 无法重连，创建新连接!")
                            self.session_dict["session_id"] = ""
                            self.session_dict["last_seq"] = 0
                            raise RuntimeError("[botpy] 无法重连")
                    else:
                        _log.warning("[botpy] 未知消息类型: %s" % msg.type)
                    if self._wss.closed:
                        _log.info("[botpy] ws关闭, 停止接收消息!")
                        break

    async def ws_identify(self):
        """websocket鉴权"""
        _log.info("[botpy] 鉴权中...")
        await self.token.check_token()
        payload = {
            "op": 2,  # 鉴权消息
            "d": {
                "shard": [
                    self.session_dict["shards"]["shard_id"],
                    self.session_dict["shards"]["shard_count"],
                ],
                "token": self.token.access_token,
                "intents": self.intents,
            },
        }

        await self.wss_send(json.dumps(payload))

    async def ws_resume(self):
        """
        websocket重连
        """
        _log.info("[botpy] 重连启动...")
        await self.token.check_token()
        payload = {
            "op": 6,  # 发起重连消息
            "d": {
                "token": self.token.access_token,
                "session_id": self.session_dict["session_id"],
                "seq": self.session_dict["last_seq"],
            },
        }

        await self.wss_send(json.dumps(payload))

    async def on_message(self, message):
        msg = json.loads(message)
        opcode = msg["op"]
        event = msg.get("t", "no event")
        _log.debug(f"[botpy] 接收消息类型: {event}, {opcode}")
        if opcode == 10:  # 首次握手消息
            if self._wss is None:
                raise Exception("[botpy] websocket连接失败")
            if self.session_dict["session_id"]:
                await self.ws_resume()
            else:
                await self.ws_identify()
            return
        elif opcode == 11:  # 心跳回复
            return
        elif opcode == 7:  # 下发重连信号
            self._can_reconnect = True
            return
        elif opcode == 9:  # 非法session
            self._can_reconnect = False
            return
        elif event and opcode == 0:  # 下发事件消息
            try:
                func = self.parser[event.lower()]
            except KeyError:
                _log.error(f"_parser unknown event: {event.lower()}")
            else:
                func(msg)
        if event == "READY":
            data = msg["d"]
            self.session_dict["session_id"] = data["session_id"]
            # session_id第一次被设置
            self.session_dict["shards"]["shard_id"] = data["shard"][0]
            self.session_dict["shards"]["shard_count"] = data["shard"][1]
            _log.info(f"[botpy] 机器人「{data["user"]['username']}」已就绪!")
        elif event == "RESUMED":
            _log.info("[botpy] 机器人重连成功! ")

        if self.heartbeat_start:
            if not self.heartbeat_task:
                self.heartbeat_task = self.loop.create_task(self._send_heart())
            self.heartbeat_start = False

    async def _send_heart(self):
        """
        心跳包
        :param interval: 间隔时间
        """
        _log.info("[botpy] 心跳维持启动...")
        while True:
            payload = {
                "op": 1,  # 发送心跳
                "d": self.session_dict["last_seq"],
            }
            await self.wss_send(json.dumps(payload))
            await asyncio.sleep(self.heartbeat_interval)

    async def wss_send(self, event_json):
        """
        websocket发送消息
        :param event_json:
        """
        _log.debug("[botpy] 发送消息: %s" % event_json)
        if isinstance(self._wss, ClientWebSocketResponse):
            if self._wss.closed or not self._wss:
                _log.error("[botpy] wss连接已关闭, 心跳检测停止!")
                self.heartbeat_start = True
            else:
                await self._wss.send_str(data=event_json)

    @property
    def client(self):
        return self

    def is_closed(self) -> bool:
        return self._closed

    def run(self, **kwargs) -> None:
        """
        机器人服务开始执行
        注意:
          这个函数必须是最后一个调用的函数，因为它是阻塞的。这意味着事件的注册或在此函数调用之后调用的任何内容在它返回之前不会执行。
        """
        try:
            self.loop.run_until_complete(self.start(**kwargs))
        except KeyboardInterrupt:
            return

    async def request(self, route: Route, retry_time=0, **kwargs) -> Coroutine:
        """
        请求API
        route: 请求路由
        kwargs: 其他参数
        """
        if retry_time > 2:
            return
        if not self.token:
            Exception("[botpy] 没有获取token...")
            return
        await self.token.check_token()
        self._headers = {
            "Authorization": self.token.access_token,  # self.token.get_string(),
            "X-Union-Appid": self.token.app_id,
        }
        _log.debug(
            f"[botpy] \n\t[{route.method}] {route.url}\n\t[Headers] {self._headers}"
        )
        if kwargs:
            _log.debug(f"[botpy] \n\t[JSON] {kwargs}")
        if not self._session or self._session.closed:
            self._session = ClientSession(
                connector=TCPConnector(limit=500, ssl=SSLContext(), force_close=True),
            )
        try:
            async with self._session.request(
                method=route.method,
                url=route.url,
                headers=self._headers,
                timeout=(aiohttp.ClientTimeout(total=5)),
                **kwargs,
            ) as response:
                return await _handle_response(response)
        except asyncio.TimeoutError:
            _log.warning(f"请求超时，请求连接: {route.url}")
        except ConnectionResetError:
            _log.debug("session connection broken retry")
            await self.request(route, retry_time + 1, **kwargs)

        # return await self.request(route, **kwargs)

    async def start(self, appid: str, secret: str) -> Coroutine:
        """机器人开始执行
        appid:  机器人 appid
        secret: 机器人 secret"""

        self.loop = asyncio.get_running_loop()
        self.token = Token(appid, secret)
        _log.info("[botpy] token初始化完成!")
        self.robot = RobotDict(await self.request(Route("GET", "/users/@me")))

        {
            "id": "17109309837717111992",
            "username": "小熊bot-测试中",
            "avatar": "http://thirdqq.qlogo.cn/g?b=oidb&k=c5JIb9DaSxSCourmvQEOqg&kti=Z7VmXgwBHsI&s=0&t=1700627345",
            "union_openid": "32E043AE2D57081DBD3530707F4AB5B1",
            "union_user_account": "",
            "share_url": "https://qun.qq.com/qunpro/robot/qunshare?robot_uin=3889002795&robot_appid=102075443&biz_type=0",
            "welcome_msg": "",
        }

        self.wss_dict = await self.request(Route("GET", "/gateway/bot"))

        {
            "url": "wss://api.sgroup.qq.com/websocket",
            "shards": 1,
            "session_start_limit": {
                "total": 1500,
                "remaining": 1452,
                "reset_after": 29469139,
                "max_concurrency": 1,
            },
        }

        # 每个机器人创建的连接数不能超过remaining剩余连接数
        _log.warning(
            f"[botpy] Remaining:{self.wss_dict['session_start_limit']['remaining']}, 最大并发：{self.wss_dict['session_start_limit']['max_concurrency']}"
        )

        # 根据限制建立分片的并发链接数
        if self.wss_dict["shards"] > 1:
            _log.warning(
                f"[botpy] 分片： {self.wss_dict['shards']}，机器人可以加新的session!"
            )
        else:
            self.session_dict = {
                "session_id": "",
                "last_seq": 0,
                "url": self.wss_dict["url"],
                "shards": {"shard_id": 0, "shard_count": 1},
            }

        """
        newConnect 启动一个新的连接，如果连接在监听过程中报错了，或者被远端关闭了链接，需要识别关闭的原因，能否继续 resume
        如果能够 resume，则往 sessionChan 中放入带有 sessionID 的 session
        如果不能，则清理掉 sessionID，将 session 放入 sessionChan 中
        session 的启动，交给 start 中的 for 循环执行，session 不自己递归进行重连，避免递归深度过深"""

        while not self._closed:
            _log.info("[botpy] 会话循环检查...")
            try:
                await self.ws_connect()
                await asyncio.sleep(10)
            except (Exception, KeyboardInterrupt, SystemExit) as e:
                if self._wss:
                    _log.error(f"[botpy] websocket连接: {self._wss}, 异常信息 : {e}")
                traceback.print_exc()
                self._closed = True
                if self._session and not self._session.closed:
                    await self._session.close()
                _log.info("[botpy] 服务意外停止!")

    def ws_dispatch(self, event: str, *args) -> None:
        """分发ws的下行事件

        解析client类的on_event事件，进行对应的事件回调
        """
        _log.info("[botpy] 调度事件: %s", event)
        method = "on_" + event

        if hasattr(self, method):
            coroutine = getattr(self, method)

            wrapper = self._run_event(coroutine, method, *args)
            self.loop.create_task(wrapper, name=f"[botpy] {method}")
        else:
            _log.error("[botpy] 事件: %s 未注册", event)

    async def _run_event(
        self,
        coroutine: Callable,
        event_name: str,
        *args,
    ) -> None:
        try:
            _log.info(f"[botpy] 运行时事件：{event_name}")
            await coroutine(*args)
        except Exception:
            traceback.print_exc()

    async def post_group_message(
        self,
        group_openid: str,
        msg_type: int = 0,
        content: str = None,
        embed: Embed = None,
        ark: Ark = None,
        message_reference: Reference = None,
        media: Media = None,
        msg_id: str = None,
        msg_seq: int = 1,
        event_id: str = None,
        markdown: MarkdownPayload = None,
        # keyboard: Keyboard = None,
    ) -> Message:
        """
        发送消息。

        注意:
        - 要求操作人在该群具有发送消息的权限。
        - 发送成功之后，会触发一个创建消息的事件。
        - 被动回复消息有效期为 5 分钟
        - 发送消息接口要求机器人接口需要链接到websocket message 上保持在线状态

        Args:
          group_openid (str): 您要将消息发送到的群的 ID。
          msg_type (int): 消息类型：0 是文本，1 图文混排，2 是 markdown，3 ark，4 embed，7 media 富媒体
          content (str): 消息的文本内容。
          embed (Embed): embed 消息，一种特殊的 ark
          ark (Ark): ark 模版消息
          message_reference (Reference): 对消息的引用。
          media (Media): 富媒体消息
          msg_id (str): 您要回复的消息的 ID。
          msg_seq (int): 回复消息的序号，与 msg_id 联合使用，默认是1。相同的 msg_id + msg_seq 重复发送会失败。
          event_id (str): 您要回复的消息的事件 ID。
          markdown (MarkdownPayload): markdown 消息
          keyboard (Keyboard): keyboard 消息

        Returns:
          Message: 一个消息字典对象。
        """
        payload = locals()
        payload.pop("self", None)
        route = Route("POST", f"/v2/groups/{group_openid}/messages")
        return await self.request(route, json=payload)

    async def post_c2c_message(
        self,
        openid: str,
        msg_type: int = 0,
        content: str = None,
        embed: Embed = None,
        ark: Ark = None,
        message_reference: Reference = None,
        media: Media = None,
        msg_id: str = None,
        msg_seq: str = 1,
        event_id: str = None,
        markdown: MarkdownPayload = None,
        # keyboard: Keyboard = None,
    ) -> Message:
        """
        发送消息。

        注意:
        - 要求操作人具有发送消息的权限。
        - 发送成功之后，会触发一个创建消息的事件。
        - 被动回复消息有效期为 5 分钟
        - 发送消息接口要求机器人接口需要链接到websocket message 上保持在线状态

        Args:
          openid (str): 您要将消息发送到的用户的 ID。
          msg_type (int): 消息类型：0 是文本，1 图文混排，2 是 markdown，3 ark，4 embed，7 media 富媒体
          content (str): 消息的文本内容。
          embed (Embed): embed 消息，一种特殊的 ark
          ark (Ark): ark 模版消息
          message_reference (Reference): 对消息的引用。
          media (Media): 富媒体消息
          msg_id (str): 您要回复的消息的 ID。
          msg_seq (int): 回复消息的序号，与 msg_id 联合使用，默认是1。相同的 msg_id + msg_seq 重复发送会失败。
          event_id (str): 您要回复的消息的事件 ID。
          markdown (MarkdownPayload): markdown 消息
          keyboard (Keyboard): keyboard 消息

        Returns:
          Message: 一个消息字典对象。
        """
        payload = locals()
        _log.info(payload)

        payload.pop("self", None)
        route = Route("POST", f"/v2/users/{openid}/messages")
        return await self.request(route, json=payload)

    async def post_group_file(
        self,
        group_openid: str,
        file_type: int,
        url: str,
        srv_send_msg: bool = False,
    ) -> Media:
        """
        上传/发送群聊图片

        Args:
          group_openid (str): 您要将消息发送到的群的 ID
          file_type (int): 媒体类型：1 图片png/jpg，2 视频mp4，3 语音silk，4 文件（暂不开放）
          url (str): 需要发送媒体资源的url
          srv_send_msg (bool): 设置 true 会直接发送消息到目标端，且会占用主动消息频次
        """
        payload = locals()
        payload.pop("self", None)
        route = Route("POST", f"/v2/groups/{group_openid}/files")
        return await self.request(route, json=payload)

    async def post_c2c_file(
        self,
        openid: str,
        file_type: int,
        url: str,
        srv_send_msg: bool = False,
    ) -> Media:
        """
        上传/发送c2c图片

        Args:
          openid (str): 您要将消息发送到的用户的 ID
          file_type (int): 媒体类型：1 图片png/jpg，2 视频mp4，3 语音silk，4 文件（暂不开放）
          url (str): 需要发送媒体资源的url
          srv_send_msg (bool): 设置 true 会直接发送消息到目标端，且会占用主动消息频次
        """
        payload = locals()
        payload.pop("self", None)
        route = Route("POST", f"/v2/users/{openid}/files")
        return await self.request(route, json=payload)

    # todo: 添加base64文件上传功能

    async def upload_grp_base64file(
        self,
        group_openid: str,
        file_type: int,
        file_data: str,
        srv_send_msg: bool = False,
    ) -> Media:
        """
        上传/发送群聊图片
        Args:
          group_openid (str): 您要将消息发送到的群的 ID
          file_type (int): 媒体类型：1 图片png/jpg，2 视频mp4，3 语音silk，4 文件（暂不开放）
          file_data (str): 二进制文件的base64编码，可用于代替网络url资源，实现本地上传文件
          srv_send_msg (bool): 设置 true 会直接发送消息到目标端，且会占用主动消息频次
        """
        payload = locals()
        payload.pop("self", None)
        route = Route("POST", f"/v2/groups/{group_openid}/files")
        return await self.request(route, json=payload)

    async def upload_c2c_base64file(
        self,
        openid: str,
        file_type: int,
        file_data: str,
        srv_send_msg: bool = False,
    ) -> Media:
        """
        上传/发送C2C图片 (基于base64)
        Args:
          openid (str): 您要将消息发送到的用户的 ID
          file_type (int): 媒体类型：1 图片png/jpg，2 视频mp4，3 语音silk，4 文件（暂不开放）
          file_data (str): 二进制文件的base64编码，可用于代替网络url资源，实现本地上传文件
          srv_send_msg (bool): 设置 true 会直接发送消息到目标端，且会占用主动消息频次
        """
        payload = {
            "openid": openid,
            "file_type": file_type,
            "file_data": file_data,
            "srv_send_msg": srv_send_msg,
        }
        route = Route("POST", f"/v2/users/{openid}/files")
        return await self.request(route, json=payload)

    def parse_ready(self, payload):
        self.ws_dispatch("ready")

    def parse_resumed(self, payload):
        self.ws_dispatch("resumed")

    # botpy.flags.Intents.public_messages
    def parse_group_at_message_create(self, payload):
        _message = GroupMessage(self, payload.get("id", None), payload.get("d", {}))
        self.ws_dispatch("group_at_message_create", _message)

    def parse_c2c_message_create(self, payload):
        _message = C2CMessage(self, payload.get("id", None), payload.get("d", {}))
        self.ws_dispatch("c2c_message_create", _message)

    def parse_friend_add(self, payload):
        _event = Event(self, payload)
        self.ws_dispatch("friend_add", _event)

    def parse_friend_del(self, payload):
        _log.error(payload)
        _event = Event(self, payload)
        self.ws_dispatch("friend_del", _event)


class Event:
    __slots__ = ("client", "id", "data", "openid", "timestamp")

    def __init__(self, client, payload: Dict):
        self.client: Client = client
        self.id = payload.get("id", None)
        self.data = self._Data(payload.get("d", {}))
        self.openid = self.data.openid
        self.timestamp = self.data.timestamp

    def __repr__(self):
        return str({items: str(getattr(self, items)) for items in self.__slots__})

    class _Data:
        def __init__(self, data):
            self.openid = data.get("openid", None)
            self.timestamp = data.get("timestamp", None)

    async def reply(self, **kwargs):
        return await self.client.post_c2c_message(
            openid=self.openid, msg_id=self.id, **kwargs
        )


class BaseMessage:
    __slots__ = (
        "client",
        "content",
        "id",
        "message_reference",
        "mentions",
        "attachments",
        "msg_seq",
        "timestamp",
        "event_id",
    )

    def __init__(self, client, event_id, data: MessagePayload):
        self.client: Client = client
        self.id = data.get("id", None)
        self.content = data.get("content", None)
        self.message_reference = self._MessageRef(data.get("message_reference", {}))
        self.mentions = [self._User(items) for items in data.get("mentions", {})]
        self.attachments = [
            self._Attachments(items) for items in data.get("attachments", {})
        ]
        self.msg_seq = data.get("msg_seq", None)  # 全局消息序号
        self.timestamp = data.get("timestamp", None)
        self.event_id = event_id

    def __repr__(self):
        return str(
            {
                items: str(getattr(self, items))
                for items in self.__slots__
                if not items.startswith("_")
            }
        )

    def __dict__(self):
        return {
            items: str(getattr(self, items))
            for items in self.__slots__
            if not items.startswith("_")
        }

    class _MessageRef:
        def __init__(self, data):
            self.message_id = data.get("message_id", None)

        def __repr__(self):
            return str(self.__dict__)

    class _Attachments:
        def __init__(self, data):
            self.content_type = data.get("content_type", None)
            self.filename = data.get("filename", None)
            self.height = data.get("height", None)
            self.width = data.get("width", None)
            self.id = data.get("id", None)
            self.size = data.get("size", None)
            self.url = data.get("url", None)

        def __repr__(self):
            return str(self.__dict__)


class GroupMessage(BaseMessage):
    __slots__ = (
        "author",
        "group_openid",
    )

    def __init__(self, client, event_id, data: MessagePayload):
        super().__init__(client, event_id, data)
        self.author = self._User(data.get("author", {}))
        self.group_openid = data.get("group_openid", None)

    def __repr__(self):
        slots = self.__slots__ + super().__slots__
        return str(
            {
                items: str(getattr(self, items))
                for items in slots
                if not items.startswith("_")
            }
        )

    def __dict__(self):
        return {
            items: str(getattr(self, items))
            for items in self.__slots__
            if not items.startswith("_")
        }

    class _User:
        def __init__(self, data):
            self.member_openid = data.get("member_openid", None)

        def __repr__(self):
            return str(self.__dict__)

    async def reply(self, **kwargs):
        return await self.client.post_group_message(
            group_openid=self.group_openid, msg_id=self.id, **kwargs
        )


class C2CMessage(BaseMessage):
    __slots__ = ("author",)

    def __init__(self, client, event_id, data: MessagePayload):
        super().__init__(client, event_id, data)

        self.author = self._User(data.get("author", {}))

    def __repr__(self):
        slots = self.__slots__ + super().__slots__
        return str(
            {
                items: str(getattr(self, items))
                for items in slots
                if not items.startswith("_")
            }
        )

    def __dict__(self):
        return {
            items: str(getattr(self, items))
            for items in self.__slots__
            if not items.startswith("_")
        }

    class _User:
        def __init__(self, data):
            self.user_openid = data.get("user_openid", None)

        def __repr__(self):
            return str(self.__dict__)

    async def reply(self, **kwargs):
        return await self.client.post_c2c_message(
            openid=self.author.user_openid, msg_id=self.id, **kwargs
        )
